…retype
ADR-0104's 2026-09-05 addendum promised that step 3 of
`os migrate files-to-references --apply` moves a media column while "aborting
on the first cell that is not a JSON string", and prescribed a Postgres
`USING` clause in the paragraph below it. Rehearsed on a live PostgreSQL
16.13, those two sentences contradict each other: the retype does not abort,
it converts, and an un-backfilled inline object is flattened to literal text
in the varchar column.
The requirement is correct and is unchanged. What was optimistic is the
statement list under it, so the step now reads as 3a (a pre-check over every
cell, before any DDL or rewrite, carrying the requirement verbatim) and 3b
(the move, only if 3a found none). The sketch paragraph records what both
rehearsed dialects actually did — Postgres flattens, SQLite silently skips —
and names the measured discriminator for each.
Confidence gap 1 gains its one Postgres exception: that single statement moves
from reasoned to measured. Gap 2 is partly closed by a rehearsal whose result
was negative. MySQL stays entirely unmeasured, and whether some other Postgres
`USING` expression could abort on its own stays an open question rather than a
finding — both are carried forward under gap 2 as open.
No decision is re-opened: D1/D3, the window and the end-state column are
untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
Fixes #16183
Clause-②: no
Part of #15989 — that card is not addressed here and remains open.
⛔ GOVERNED SURFACE — this PR is a TERMINAL DRAFT
The diff's only path is
docs/adr/0104-field-runtime-value-shape-contract.md, a governed surface under Prime Directive #14. ⇒ this PR is docs-only, stays a draft, and is merged by a human. It is never marked ready for review, never enqueued, and auto-merge is never armed on it. No code rides along;content/docs/releases/**is untouched.What the diff does
ADR-0104's 2026-09-05 addendum promises that step 3 of
os migrate files-to-references --applymoves a media column while "aborting on the first cell that is not a JSON string", and the paragraph below it prescribes a PostgresUSINGclause that "unquotes the JSON string". Those two sentences contradict each other.The requirement is correct and is unchanged — the phrase survives byte-for-byte (verified whitespace-normalised: exactly one occurrence, before and after). What was optimistic is the statement list under it. Three edits:
3a(a pre-check over every cell of every media column the step is about to move, run before any DDL or rewrite, carrying the requirement phrase verbatim) and3b(the move, run only if 3a found none).The rehearsals — re-run here, not cited from triage
Triage stated it did not re-run them, so both were re-run in this container before being written into a governed document as normative statements.
Tree: branch
claude/issue-16183-adr-0104-step3-abort-precheck, baseorigin/mainatbc1c1ce494, final commitc155e20a78.Postgres — server version
PostgreSQL 16.13 (Ubuntu 16.13-0ubuntu0.24.04.1) on x86_64-pc-linux-gnu. Ajsoncolumn holding three JSON-quoted ids plus one un-backfilled inline object. Run underpsql -v ON_ERROR_STOP=1, whose exit 0 is itself the proof no statement aborted:SQLite —
sqlite_version()=3.51.2(node:sqlite, Node v22.22.2), same four-cell shape plus an already-bare cell. Thejson_type='text'gate converted the two quoted cells (changes = 2), left the already-bare cell untouched, was idempotent on re-run (before === aftertrue) — and left the inline-object cell silently unconverted rather than refusing it.⇒ Neither rehearsed statement performs the abort. The two failure modes differ (Postgres destroys that cell's structure; SQLite leaves it in the old encoding) and neither is the stated abort. That is the whole basis for 3a.
Both are written into the ADR as still open, and neither is "completed":
#>> '{}'is the idiom the ADR's words describe and it does not abort; the new text calls anything else an open question, not a finding.The ADR's decision is not re-opened: D1/D3, the window and the end-state column are untouched.
Changeset route — measured, not assumed
Route chosen:
skip-changeset. The measurement, not the assumption that "a docs path publishes nothing":git diff --name-onlyagainst the merge base = exactly 1 path,docs/adr/0104-field-runtime-value-shape-contract.md.pnpm-workspace.yamlcoversdocs/**(the globs arepackages/*,packages/apps/*,packages/drivers/*,packages/plugins/*,packages/qa/*,packages/triggers/*,packages/services/*,packages/adapters/*,packages/connectors/*,apps/*,examples/*) ⇒ the path belongs to no workspace package.package.jsonis the repo root,@objectstack/spec-monorepo,private: true⇒ not a released package. There is nodocs/package.json.git ls-files: 70 non-private namedpackage.jsonfiles. None is an ancestor directory ofdocs/adr, and none has afiles[]entry containingdocsor.., so no published artifact can reach the path.⇒ this diff publishes nothing from any released package, which is exactly the condition AGENTS.md's changeset floor sets for the label (that sentence is at
AGENTS.md:1036onorigin/mainand on this branch; the same sentence sits at:1019in the shared checkout).Gates — COVERAGE and VERDICTS, stated separately
Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, re-derived after a freshgit fetch origin main(byte-identical command list both times).COVERAGE (what was accounted for, independent of pass/fail):
Not covered locally, by design: the always-runs tail, the unreachable listing, the two families taking a value from the workflow, and the 13 families that only apply once a changeset path exists — this PR deliberately has none. CI owns those.
VERDICTS (exit codes captured by redirect-then-
$?, never across a pipe —cmd > log 2>&1; EXIT=$?):That last one first returned EXIT=3
PREREQUISITE NOT MET(@objectstack/formulaand@objectstack/lintnot built). Exit 3 is NOT MEASURED, not a pass, so the prerequisite was satisfied —pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint --concurrency=2under the shared verify lock,VERDICT command-exit 0— and the gate re-run to a real EXIT=0.Repo-wide eslint (
pnpm lint=eslint . --no-inline-config) is CI's run, and the narrowing here is a measurement rather than a skip:eslint --format jsonon the changed file reports"File ignored because no matching configuration was supplied.";eslint.config.mjsdeclares its file kinds as{ts,tsx,mts,cts,js,jsx,mjs,cjs}with zero markdown.--format json: 1 file examined,errorCount: 0.package.json, and its one path is a.mdeslint never reads ⇒ no untouched file's verdict can move.Ablation: inapplicable. The diff adds no guard, no schema and no executable line — it is prose in a governed document. There is nothing to mutate and no test whose failure could be demonstrated, so none was invented.
Escalation antecedent — re-checked cheaply, unchanged
Triage set: 「若发现已有实现按现文字写成,立即抬到 p1 并回帖」. Re-checked on
bc1c1ce494rather than re-derived:runFilesToReferencesMigrationis still backfill → verify → record-flag, andunquote,JSON_UNQUOTE, the Postgres text-extract operator,json_extractandALTER COLUMNall return 0 hits inpackages/services/service-storage/src/files-to-references-migration.tsandpackages/cli/src/commands/migrate/files-to-references.ts. ⇒ step 3 is still unbuilt, nothing implements the old wording, p2 holds.维护者速读(草稿)
改了什么
只改了一份治理文档
docs/adr/0104-field-runtime-value-shape-contract.md里的一段执行说明。原文承诺迁移第 3 步会「遇到第一个非 JSON 字符串的单元格就中止」,紧接着开的 Postgres 方子却做不到这件事。现在把中止拆成 3a(动任何 DDL 之前先跑的预检)和 3b(真正的搬列动作),并把两次实测结果写进置信缺口。⛔ 需求原话一字未改,决定也一个字没动。为什么改
这段文字挡着 step 3 的实现。照现在的写法去实现,那条
ALTER TABLE … USING语句会静默压平恰恰是回填没能转换的那些行 —— 也就是对账存在的意义所在。今天还没有人实现 step 3(本轮已复核),所以这是修正它成本最低的时刻:改文字,不改数据,不改代码。风险与代价(含回滚)
风险极低:单文件、纯文字、零代码、零发布物。回滚 = revert 这一个提交,没有任何下游产物需要跟着退。真正的代价在不改这一侧:留着它,下一个实现者会写出那条破坏性语句。
席位意见
(留空,待席位定稿)
你要做的
这是治理面 ⇒ 本 PR 恒为 draft,由维护者人工合并。请确认三件事:① 需求原话确实一字未改;② MySQL 与「别的 Postgres 写法能否自行中止」两处仍标为未测,没有被顺手补全;③ 3a/3b 的拆法读起来就是你要的执行顺序。确认后人工直合。
Generated by Claude Code